unit DW.REST.Json.Helpers;

{*******************************************************}
{                                                       }
{                      Kastri                           }
{                                                       }
{         Delphi Worlds Cross-Platform Library          }
{                                                       }
{    Copyright 2020 Dave Nottage under MIT license      }
{  which is located in the root folder of this library  }
{                                                       }
{*******************************************************}

{$I DW.GlobalDefines.inc}

interface

uses
  // RTL
  System.JSON, REST.Json;

type
  TJsonHelper = class helper for TJson
  public
    class function FileToObject<T: class, constructor>(out AObject: T; const AFileName: string;
      AOptions: TJsonOptions = [joDateIsUTC, joDateFormatISO8601]): Boolean; static;
    class procedure SaveToFile(const AObject: TObject; const AFileName: string); static;
    /// <summary>
    ///   Replacement functions for TJson.Format, which does not format correctly
    /// </summary>
    /// <remarks>
    ///   Overload that takes a string parameter has been modified so that it should also work for JSON which is not generated by TJsonValue.ToString
    ///   Please use this function with CAUTION!! It still may not cater for all situations, and is still not perfect
    /// </remarks>
    class function Tidy(const AJsonValue: TJsonValue; const AIndentSize: Integer = 2): string; overload; static;
    class function Tidy(const AJson: string; const AIndentSize: Integer = 2): string; overload; static;
  end;

implementation

uses
  // RTL
  System.Character, System.SysUtils, System.IOUtils;

class function TJsonHelper.FileToObject<T>(out AObject: T; const AFileName: string;
  AOptions: TJsonOptions = [joDateIsUTC, joDateFormatISO8601]): Boolean;
var
  LJSON: string;
begin
  Result := False;
  LJSON := '';
  if TFile.Exists(AFileName) then
    LJSON := TFile.ReadAllText(AFileName);
  if not LJSON.IsEmpty then
  begin
    AObject := JsonToObject<T>(LJSON, AOptions);
    Result := AObject <> nil;
  end;
end;

class procedure TJsonHelper.SaveToFile(const AObject: TObject; const AFileName: string);
var
  LJSON: string;
begin
  if AObject = nil then
    LJSON := '{}'
  else
    LJSON := Tidy(ObjectToJsonString(AObject));
  TFile.WriteAllText(AFileName, LJSON);
end;

class function TJsonHelper.Tidy(const AJsonValue: TJsonValue; const AIndentSize: Integer = 2): string;
begin
  Result := Tidy(AJsonValue.ToString, AIndentSize);
end;

// Now based on: https://pastebin.com/Juks92Y2 (if the link still exists), by Lars Fosdal
class function TJsonHelper.Tidy(const AJson: string; const AIndentSize: Integer = 2): string;
const
  cEOL = #13#10;
var
  LChar: Char;
  LIsInString: boolean;
  LIsEscape: boolean;
  LIsHandled: boolean;
  LIndent: Integer;
begin
  Result := '';
  LIndent := 0;
  LIsInString := False;
  LIsEscape := False;
  for LChar in AJson do
  begin
    if not LIsInString then
    begin
      LIsHandled := False;
      if (LChar = '{') or (LChar = '[') then
      begin
        Inc(LIndent);
        Result := Result + LChar + cEOL + StringOfChar(' ', LIndent * AIndentSize);
        LIsHandled := True;
      end
      else if LChar = ',' then
      begin
        Result := Result + LChar + cEOL + StringOfChar(' ', LIndent * AIndentSize);
        LIsHandled := True;
      end
      else if (LChar = '}') or (LChar = ']') then
      begin
        Dec(LIndent);
        Result := Result + cEOL + StringOfChar(' ', LIndent * AIndentSize) + LChar;
        LIsHandled := True;
      end;
      if not LIsHandled and not LChar.IsWhiteSpace then
        Result := Result + LChar;
    end
    else
      Result := Result + LChar;
    if not LIsEscape and (LChar = '"') then
      LIsInString := not LIsInString;
    LIsEscape := (LChar = '\') and not LIsEscape;
  end;
end;

end.
